1   /*
2    * Copyright (C) 2007 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.google.common.eventbus;
18  
19  import com.google.common.base.Objects;
20  import com.google.common.base.Throwables;
21  import com.google.common.cache.CacheBuilder;
22  import com.google.common.cache.CacheLoader;
23  import com.google.common.cache.LoadingCache;
24  import com.google.common.collect.HashMultimap;
25  import com.google.common.collect.ImmutableList;
26  import com.google.common.collect.Maps;
27  import com.google.common.collect.Multimap;
28  import com.google.common.reflect.TypeToken;
29  import com.google.common.util.concurrent.UncheckedExecutionException;
30  
31  import java.lang.reflect.Method;
32  import java.util.Arrays;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  
37  import javax.annotation.Nullable;
38  
39  /**
40   * A {@link SubscriberFindingStrategy} for collecting all event subscriber methods that are marked
41   * with the {@link Subscribe} annotation.
42   *
43   * @author Cliff Biffle
44   * @author Louis Wasserman
45   */
46  class AnnotatedSubscriberFinder implements SubscriberFindingStrategy {
47    /**
48     * A thread-safe cache that contains the mapping from each class to all methods in that class and
49     * all super-classes, that are annotated with {@code @Subscribe}. The cache is shared across all
50     * instances of this class; this greatly improves performance if multiple EventBus instances are
51     * created and objects of the same class are registered on all of them.
52     */
53    private static final LoadingCache<Class<?>, ImmutableList<Method>> subscriberMethodsCache =
54        CacheBuilder.newBuilder()
55            .weakKeys()
56            .build(new CacheLoader<Class<?>, ImmutableList<Method>>() {
57              @Override
58              public ImmutableList<Method> load(Class<?> concreteClass) throws Exception {
59                return getAnnotatedMethodsInternal(concreteClass);
60              }
61            });
62  
63    /**
64     * {@inheritDoc}
65     *
66     * This implementation finds all methods marked with a {@link Subscribe} annotation.
67     */
68    @Override
69    public Multimap<Class<?>, EventSubscriber> findAllSubscribers(Object listener) {
70      Multimap<Class<?>, EventSubscriber> methodsInListener = HashMultimap.create();
71      Class<?> clazz = listener.getClass();
72      for (Method method : getAnnotatedMethods(clazz)) {
73        Class<?>[] parameterTypes = method.getParameterTypes();
74        Class<?> eventType = parameterTypes[0];
75        EventSubscriber subscriber = makeSubscriber(listener, method);
76        methodsInListener.put(eventType, subscriber);
77      }
78      return methodsInListener;
79    }
80  
81    private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) {
82      try {
83        return subscriberMethodsCache.getUnchecked(clazz);
84      } catch (UncheckedExecutionException e) {
85        throw Throwables.propagate(e.getCause());
86      }
87    }
88  
89    private static final class MethodIdentifier {
90      private final String name;
91      private final List<Class<?>> parameterTypes;
92  
93      MethodIdentifier(Method method) {
94        this.name = method.getName();
95        this.parameterTypes = Arrays.asList(method.getParameterTypes());
96      }
97  
98      @Override
99      public int hashCode() {
100       return Objects.hashCode(name, parameterTypes);
101     }
102 
103     @Override
104     public boolean equals(@Nullable Object o) {
105       if (o instanceof MethodIdentifier) {
106         MethodIdentifier ident = (MethodIdentifier) o;
107         return name.equals(ident.name) && parameterTypes.equals(ident.parameterTypes);
108       }
109       return false;
110     }
111   }
112 
113   private static ImmutableList<Method> getAnnotatedMethodsInternal(Class<?> clazz) {
114     Set<? extends Class<?>> supers = TypeToken.of(clazz).getTypes().rawTypes();
115     Map<MethodIdentifier, Method> identifiers = Maps.newHashMap();
116     for (Class<?> superClazz : supers) {
117       for (Method superClazzMethod : superClazz.getMethods()) {
118         if (superClazzMethod.isAnnotationPresent(Subscribe.class)
119             && !superClazzMethod.isBridge()) {
120           Class<?>[] parameterTypes = superClazzMethod.getParameterTypes();
121           if (parameterTypes.length != 1) {
122             throw new IllegalArgumentException("Method " + superClazzMethod
123                 + " has @Subscribe annotation, but requires " + parameterTypes.length
124                 + " arguments.  Event subscriber methods must require a single argument.");
125           }
126 
127           MethodIdentifier ident = new MethodIdentifier(superClazzMethod);
128           if (!identifiers.containsKey(ident)) {
129             identifiers.put(ident, superClazzMethod);
130           }
131         }
132       }
133     }
134     return ImmutableList.copyOf(identifiers.values());
135   }
136 
137   /**
138    * Creates an {@code EventSubscriber} for subsequently calling {@code method} on
139    * {@code listener}.
140    * Selects an EventSubscriber implementation based on the annotations on
141    * {@code method}.
142    *
143    * @param listener  object bearing the event subscriber method.
144    * @param method  the event subscriber method to wrap in an EventSubscriber.
145    * @return an EventSubscriber that will call {@code method} on {@code listener}
146    *         when invoked.
147    */
148   private static EventSubscriber makeSubscriber(Object listener, Method method) {
149     EventSubscriber wrapper;
150     if (methodIsDeclaredThreadSafe(method)) {
151       wrapper = new EventSubscriber(listener, method);
152     } else {
153       wrapper = new SynchronizedEventSubscriber(listener, method);
154     }
155     return wrapper;
156   }
157 
158   /**
159    * Checks whether {@code method} is thread-safe, as indicated by the
160    * {@link AllowConcurrentEvents} annotation.
161    *
162    * @param method  subscriber method to check.
163    * @return {@code true} if {@code subscriber} is marked as thread-safe,
164    *         {@code false} otherwise.
165    */
166   private static boolean methodIsDeclaredThreadSafe(Method method) {
167     return method.getAnnotation(AllowConcurrentEvents.class) != null;
168   }
169 }